package com.mvc.app.utils;

import com.container.app.MContainer;
import com.container.app.exceptions.MContainerUncheckedException;
import com.mvc.app.ActionRequest;
import com.mvc.app.Model;
import com.mvc.app.ModelView;
import com.mvc.app.annotations.BeanName;
import com.mvc.app.annotations.ControllerMethod;
import com.mvc.app.annotations.PropertyName;
import com.mvc.app.exceptions.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * Created with IntelliJ IDEA.
 * User: Georgiana.Scarlat
 * Date: 8/27/12
 * Time: 12:04 PM
 */
public class MVCUtils {

    public static final String BEAN = "bean";

    public static final int FROM_SECOND_ELEMENT = 1;
    public static final String SETTER_STRING = "set";
    private static final String GETTER_STRING = "get";
    public static final String PATH_SEPARATOR = "/";
    public static final String ALL_EXCEPT_FILE_SEPARAOR_REGEX = "[^" + PATH_SEPARATOR + "]";
    public static final String EMPTY_STRING = "";
    public static final Class<ControllerMethod> CONTROLLER_METHOD_ANNOTATION_CLASS = ControllerMethod.class;
    public static final String NO_COMPONENT_MESSAGE = "There is no component with name ";
    public static final String NAME_METHOD = "name";
    public static final Class<? extends Annotation> BEAN_ANNOTATION_CLASS = BeanName.class;
    public static final Class<? extends Annotation> BEAN_PROPERTY_CLASS = PropertyName.class;


    public static String getBeanName(HttpServletRequest request) {

        return request.getParameter(BEAN);
    }


    public static Map<String, Object> getBeanInstances(MContainer container, HttpServletRequest request) throws IncompleteModelException {

        Map<String, Object> beans = new HashMap<String, Object>();
        String beanName;

        if((beanName = getBeanName(request)) == null)
            return beans;

        InvalidModelException invalidModelException = new InvalidModelException("There is no model with name " + beanName);
        Object instance = getValidComponentInstanceFromContainer(container, beanName, invalidModelException);
        Map<String, Object> leftOutProperties = new HashMap<String, Object>();

        injectBeanWithPropertyValues(request, instance, leftOutProperties);

        beans.put(beanName, instance);

        if (leftOutProperties.size() > 0)
            throw new IncompleteModelException("Incomplete model " + beanName, leftOutProperties, beans);

        return beans;
    }

    private static void injectBeanWithPropertyValues(HttpServletRequest request, Object instance, Map<String, Object> leftOutProperties) {

        String paramName, paramValue;
        @SuppressWarnings("unchecked") Enumeration<String> paramNames = request.getParameterNames();

        while (paramNames.hasMoreElements()) {
            paramName = paramNames.nextElement();
            paramValue = request.getParameter(paramName);
            try {
                processProperty(paramName, paramValue, instance);
            } catch (IncompleteModelException e) {
                leftOutProperties.put(paramName, paramValue);
            }
        }
    }

    private static <T> T getValidComponentInstanceFromContainer(MContainer container, String componentName, InvalidRequestException exception) {

        T instance;

        try {
            instance = container.getInstance(componentName);

            verifyInstance(exception, instance);

            return instance;

        } catch (MContainerUncheckedException e) {
            String errorMessage = NO_COMPONENT_MESSAGE + componentName;
            if (errorMessage.equals(e.getMessage()))
                throw exception;
            throw e;
        }
    }

    private static <T> void verifyInstance(InvalidRequestException exception, T instance) {

        if (instance == null || !instance.getClass().isAnnotationPresent(exception.getExpectedAnnotationClass()))
            throw exception;
    }

    private static void processProperty(String paramName, String paramValue, Object instance) throws IncompleteModelException {

        if (paramName.equals(BEAN))
            return;

        validatePropertyValue(paramValue);
        setFieldInObject(instance, paramName, paramValue);
    }

    private static void validatePropertyValue(String paramValue) {

        if (paramValue == null || EMPTY_STRING.equals(paramValue))
            throw new InvalidModelException("Empty model properties are not allowed.");
    }

    static void setFieldInObject(Object instance, String paramName, String paramValue) throws IncompleteModelException {

        try {
            setFieldInObjectHelper(instance, paramName, paramValue);

        } catch (NoSuchMethodException e) {
            throw new IncompleteModelException(e);
        } catch (InvocationTargetException e) {
            throw new InvalidModelException("Invalid setter for field " + paramName, e);
        } catch (IllegalAccessException e) {
            throw new InvalidModelException("Inaccessible setter for field " + paramName, e);
        }

    }

    private static void setFieldInObjectHelper(Object instance, String paramName, String paramValue) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, IncompleteModelException {

        String setterName = SETTER_STRING + paramName.toUpperCase().charAt(0) + paramName.substring(1);
        Class paramClass = getFieldClass(instance.getClass(), paramName);
        @SuppressWarnings("unchecked") Object paramConvertedValue = new TypeTransformer<String, Object>().transform(paramValue, paramClass);

        Method setterMethod = instance.getClass().getDeclaredMethod(setterName, paramClass);
        setterMethod.setAccessible(true);
        setterMethod.invoke(instance, paramConvertedValue);

    }

    static Class getFieldClass(Class<?> aClass, String fieldName) throws IncompleteModelException {

        try {
            Field field = aClass.getDeclaredField(fieldName);
            return field.getType();

        } catch (NoSuchFieldException e) {
            throw new IncompleteModelException(e);
        }
    }

    public static String getControllerNameFromPath(String pathInfo) {

        if (!isValidPath(pathInfo))
            throw new InvalidRequestPathException("The requested URL is invalid "+pathInfo);

        return pathInfo.substring(FROM_SECOND_ELEMENT, pathInfo.indexOf(PATH_SEPARATOR, FROM_SECOND_ELEMENT));
    }

    private static boolean isValidPath(String pathInfo) {

        return pathInfo.replaceAll(ALL_EXCEPT_FILE_SEPARAOR_REGEX, EMPTY_STRING).length() == 2;
    }

    public static String getControllerMethodNameFromPath(String pathInfo) {

        if (!isValidPath(pathInfo))
            throw new InvalidRequestPathException("The requested URL is invalid.");

        return pathInfo.substring(1 + pathInfo.indexOf(PATH_SEPARATOR, FROM_SECOND_ELEMENT));

    }


    public static Method getMethodWithAnnotationName(Class<?> aClass, String methodName) {

        Method methods[] = aClass.getMethods();

        for (Method method : methods) {

            try {
                if (methodMatchesAnnotationName(methodName, method)) return method;

            } catch (NoSuchMethodException e) {
                throw new InvalidControllerAnnotationException("UserController method " + methodName + " is not annotated correctly.", e);
            } catch (InvocationTargetException e) {
                throw new InvalidControllerAnnotationException("UserController method " + methodName + " is not annotated correctly.", e);
            } catch (IllegalAccessException e) {
                throw new InvalidControllerAnnotationException("UserController method " + methodName + " is not annotated correctly.", e);
            }
        }

        throw new InvalidRequestPathException("No method from " + aClass.getName() + " matches controller methodName " + methodName);
    }


    public static <T> T getControllerInstance(MContainer container, String controllerName) {

        InvalidRequestPathException requestPathException = new InvalidRequestPathException("There is no controller with name " + controllerName);

        return getValidComponentInstanceFromContainer(container, controllerName, requestPathException);
    }

    private static boolean methodMatchesAnnotationName(String methodName, Method method) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {

        Annotation annotation = method.getAnnotation(CONTROLLER_METHOD_ANNOTATION_CLASS);

        if (annotation != null) {
            String foundMethodName = getAnnotationNameMethodValue(annotation);
            if (methodName.equals(foundMethodName))
                return true;
        }
        return false;
    }

    private static String getAnnotationNameMethodValue(Annotation annotation) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {

        Method annotationNameMethod = annotation.annotationType().getDeclaredMethod(NAME_METHOD);
        annotationNameMethod.setAccessible(true);
        return (String) annotationNameMethod.invoke(annotation);
    }


    public static void validateActionRequest(ActionRequest action) {

        if (isIncompleteActionRequest(action) || isInconsistentActionRequest(action))
            throw new IllegalStateException("Can't use an incomplete action request.");

        validateControllerMethod(action.getControllerMethod());

    }

    private static boolean isInconsistentActionRequest(ActionRequest action) {
        Method controllerMethod = action.getControllerMethod();
        Object controller = action.getController();
        return !controllerMethod.getDeclaringClass().isAssignableFrom(controller.getClass());
    }

    private static boolean isIncompleteActionRequest(ActionRequest action) {
        return action == null ||
                action.getController() == null ||
                action.getControllerMethod() == null ||
                action.getRequest() == null ||
                action.getResponse() == null;
    }


    public static void validateControllerMethod(Method controllerMethod) {
        if (!isValidControllerMethod(controllerMethod))
            throw new InvalidControllerMethodException("The controller method " + controllerMethod.getName() + " is invalid.");
    }


    private static boolean isValidControllerMethod(Method controllerMethod) {

        return isAnnotatedAsControllerMethod(controllerMethod) && hasExpectedReturnTypeAndParameters(controllerMethod);

    }

    private static boolean hasExpectedReturnTypeAndParameters(Method controllerMethod) {

        Class<?>[] parameterTypes = controllerMethod.getParameterTypes();

        return returnsModelView(controllerMethod) && areHttpServletParameters(parameterTypes);
    }

    private static boolean areHttpServletParameters(Class<?>[] parameterTypes) {

        return parameterTypes.length > 1 && isAHttpServletRequest(parameterTypes[0]) && isAHttpServletResponse(parameterTypes[1]);
    }

    private static boolean isAnnotatedAsControllerMethod(Method controllerMethod) {

        return controllerMethod.isAnnotationPresent(CONTROLLER_METHOD_ANNOTATION_CLASS);
    }

    private static boolean isAHttpServletResponse(Class<?> parameterType) {

        return HttpServletResponse.class.isAssignableFrom(parameterType);
    }

    private static boolean isAHttpServletRequest(Class<?> parameterType) {

        return HttpServletRequest.class.isAssignableFrom(parameterType);
    }

    private static boolean returnsModelView(Method controllerMethod) {

        return ModelView.class.isAssignableFrom(controllerMethod.getReturnType());
    }

    public static String getAnnotationNameValue(Annotation annotation) {

        try {
            return getAnnotationNameMethodValue(annotation);

        } catch (NoSuchMethodException e) {
            throw new InvalidControllerAnnotationException("UserController method  is not annotated correctly.", e);
        } catch (InvocationTargetException e) {
            throw new InvalidControllerAnnotationException("UserController method  is not annotated correctly.", e);
        } catch (IllegalAccessException e) {
            throw new InvalidControllerAnnotationException("UserController method  is not annotated correctly.", e);
        }
    }

    public static boolean isBeanOrPropertyAnnotation(Annotation annotation) {
        return isBeanAnnotation(annotation) ||
                isPropertyAnnotation(annotation);
    }

    public static boolean isPropertyAnnotation(Annotation annotation) {
        return BEAN_PROPERTY_CLASS.isAssignableFrom(annotation.getClass());
    }

    public static boolean isBeanAnnotation(Annotation annotation) {
        return BEAN_ANNOTATION_CLASS.isAssignableFrom(annotation.getClass());
    }

    public static void validateNonEmptyModel(Model model) {
        if (model == null || model.isEmpty())
            throw new InvalidModelException("There is no model provided in the request.");
    }

    public static Object getFiledUsingGetter(Object instance, String fieldName) {

        String getterName = GETTER_STRING + fieldName.toUpperCase().charAt(0) + fieldName.substring(1);

        try {

            Method getterMethod = instance.getClass().getDeclaredMethod(getterName);
            getterMethod.setAccessible(true);
            return getterMethod.invoke(instance);

        } catch (InvocationTargetException e) {
            throw new InvalidModelException("Model method " + getterName + " throws an exception.", e);
        } catch (NoSuchMethodException e) {
            throw new InvalidControllerMethodException("Controller method needs field " + fieldName + " from " + instance.getClass().getName() + " that has no getter.", e);
        } catch (IllegalAccessException e) {
            throw new InvalidModelException("Ca't access getter method " + getterName, e);
        }


    }
}
